확률론적 언어 모형

확률론적 언어 모형(Probabilistic Language Model)은 $m$개의 단어 $w_1, w_2, \ldots, w_m$ 열(word sequence)이 주어졌을 때 문장으로써 성립될 확률 $P(w_1, w_2, \ldots, w_m)$ 을 출력함으로써 이 단어 열이 실제로 현실에서 사용될 수 있는 문장(sentence)인지를 판별하는 모형이다.

이 확률은 각 단어의 확률과 단어들의 조건부 확률을 이용하여 다음과 같이 계산할 수 있다.

$$ \begin{eqnarray} P(w_1, w_2, \ldots, w_m) &=& P(w_1, w_2, \ldots, w_{m-1}) \cdot P(w_m\;|\; w_1, w_2, \ldots, w_{m-1}) \\ &=& P(w_1, w_2, \ldots, w_{m-2}) \cdot P(w_{m-1}\;|\; w_1, w_2, \ldots, w_{m-2}) \cdot P(w_m\;|\; w_1, w_2, \ldots, w_{m-1}) \\ &=& P(w_1) \cdot P(w_2 \;|\; w_1) \cdot P(w_3 \;|\; w_1, w_2) P(w_4 \;|\; w_1, w_2, w_3) \cdots P(w_m\;|\; w_1, w_2, \ldots, w_{m-1}) \end{eqnarray} $$

여기에서 $P(w_m\;|\; w_1, w_2, \ldots, w_{m-1})$ 은 지금까지 $w_1, w_2, \ldots, w_{m-1}$라는 단어 열이 나왔을 때, 그 다음 단어로 $w_m$이 나올 조건부 확률을 말한다. 여기에서 지금까지 나온 단어를 문맥(context) 정보라고 한다.

이 때 조건부 확률을 어떻게 모형화하는냐에 따라

  • 유니그램 모형 (Unigram Model)
  • 바이그램 모형 (Bigram Model)
  • N-그램 모형 (N-gram Model)

등으로 나뉘어 진다.

유니그램 모형 (Unigram Model)

만약 모든 단어의 활용이 완전히 서로 독립이라면 단어 열의 확률은 다음과 같이 각 단어의 확률의 곱이 된다. 이러한 모형을 유니그램 모형 (Unigram Model)이라고 한다.

$$ P(w_1, w_2, \ldots, w_m) = \prod_{i=1}^m P(w_i) $$

바이그램 모형 (Bigram Model)

만약 단어의 활용이 바로 전 단어에만 의존한다면 단어 열의 확률은 다음과 같다. 이러한 모형을 Bigram 모형 또는 마코프 모형(Markov Model)이라고 한다.

$$ P(w_1, w_2, \ldots, w_m) = P(w_1) \prod_{i=2}^{m} P(w_{i}\;|\; w_{i-1}) $$

N-그램 모형 (N-gram Model)

만약 단어의 활용이 바로 전 $n$개의 단어에만 의존한다면 단어 열의 확률은 다음과 같다. 이러한 모형을 N-gram 모형이라고 한다.

$$ P(w_1, w_2, \ldots, w_m) = P(w_1) \prod_{i=n}^{m} P(w_{i}\;|\; w_{i-1}, \ldots, w_{i-n}) $$

확률 추정 방법

실제 텍스트 코퍼스(corpus)에서 확률을 추정하는 방법은 다음과 같다. 여기에서는 바이그램의 경우를 살펴본다.

일단 모든 문장에 문장의 시작과 끝을 나타내는 특별 토큰을 추가한다. 예를 들어 문장의 시작은 SS, 문장의 끝은 SE 이라는 토큰을 사용할 수 있다.

바이그램 모형에서는 전체 문장의 확률은 다음과 같이 조건부 확률의 곱으로 나타난다.

$$ P(\text{SS I am a boy SE}) = P(\text{I}\;|\; \text{SS}) \cdot P(\text{am}\;|\; \text{I}) \cdot P(\text{a}\;|\; \text{am}) \cdot P(\text{boy}\;|\; \text{a}) \cdot P(\text{SE}\;|\; \text{boy}) $$

조건부 확률은 다음과 같이 추정한다.

$$ P(w_{i}\;|\; w_{i-1}) = \dfrac{C(w_{i}, w_{i-1})}{C(w_{i-1})} $$

위 식에서 $C(w_{i}, w_{i-1})$은 전체 코퍼스에서 $(w_{i}, w_{i-1})$라는 바이그램이 나타나는 횟수이고 $C(w_{i-1})$은 전체 코퍼스에서 $(w_{i-1})$라는 유니그램(단어)이 나타나는 횟수이다.

예제

다음은 nltk 패키지의 샘플 코퍼스인 movie_reviews의 텍스트를 기반으로 N-그램 모형을 추정하고 모형 확률로부터 랜덤하게 문장을 생성하는 예제이다. 다음 문헌을 참고하여 일부 수정하였다.

우선 다음과 같이 문장(단어 리스트)의 리스트를 만든다.


In [7]:
from nltk.corpus import movie_reviews

# 문서를 문장으로 분리
sentences = list(movie_reviews.sents())

import random
# 섞는다.
random.seed(1)
random.shuffle(sentences)

In [8]:
sentences[0]


Out[8]:
[u'we',
 u'never',
 u'get',
 u'any',
 u'significant',
 u'insight',
 u'into',
 u'the',
 u'girls',
 u'and',
 u'why',
 u'they',
 u'might',
 u'be',
 u'feeling',
 u'what',
 u'they',
 u"'",
 u're',
 u'feeling',
 u'.']

이제 이 입력으로부터 확률값을 추정한다.


In [9]:
import collections, math
from math import log
from collections import Counter
from konlpy.utils import pprint


def stringify_context(context):
    return(" ".join(context))


boundaryToken = "</s>"
def ngrams(n, sentences, boundaryToken=boundaryToken, verbose=False):
    c = {}
    q = []
    for i in range(n-1):
        q.append(boundaryToken)
    for sentence in sentences:
        for w in sentence + [boundaryToken]:
            context_gram = stringify_context(q)
            if verbose:
                print(q)
                print(context_gram)
                print(w)
            if not context_gram in c:
                c[context_gram] = Counter()
            c[context_gram][w] += 1
            q.pop(0)
            q.append(w)
    return(c)

In [15]:
ngrams(2, sentences[:1000])["we"]


Out[15]:
Counter({u"'": 4,
         u'all': 1,
         u'are': 1,
         u'can': 3,
         u'don': 2,
         u'expect': 1,
         u'feel': 3,
         u'first': 1,
         u'follow': 1,
         u'get': 3,
         u'have': 1,
         u'know': 1,
         u'learn': 1,
         u'meet': 1,
         u'men': 1,
         u'might': 1,
         u'never': 1,
         u'observe': 1,
         u'probably': 1,
         u'see': 2,
         u'sit': 1,
         u'thought': 1,
         u'understand': 1,
         u'were': 1})

In [47]:
class BigramModel:
    
    def __init__(self, training_sentences, smoothing='none'):
        train = ngrams(2, training_sentences)
        self.probs = {}
        if smoothing == 'none':
            for context_gram in train.keys():
                N = sum(train[context_gram].values())
                self.probs[context_gram] = Counter({k:v/N for k,v in train[context_gram].items()})

    def prob(self, word, context):
        """takes a word string and a context which is a list of word strings, and returns the probability of the word"""
        c = stringify_context(context)
        return(self.probs[c][word])

    def scoreSentence(self, sentence, verbose=False):
        context = [boundaryToken]
        result = 0
        for w in sentence + [boundaryToken]:
            lp = log(self.prob(w, context))
            result = result + lp
            if verbose:
                pprint([context, w, lp])
            context = [w]
        return result

    def generateSentence(self, verbose=False, goryDetails=False):
        context = [boundaryToken]
        result = []
        w = None
        while not w == boundaryToken:
            r = random.random() # returns a random float between 0 and 1
            x = 0
            c = self.probs[stringify_context(context)] # this will be a Counter
            w = c.keys()[np.argmax(np.random.multinomial(1, c.values(), (1,))[0])]
            result.append(w)
            context = [w]
            if verbose:
                print(w)
        result.pop() # drop the boundary token
        return result

In [46]:
m = BigramModel(sentences)

트레이닝이 끝나면 조건부 확률의 값을 보거나 샘플 문장을 입력해서 문장의 로그 확률을 구할 수 있다.

"i" 라는 단어가 나온 뒤에 "am"이라는 단어가 나올 확률을 계산하면


In [24]:
m.prob("am", ["i"])


Out[24]:
0.018562267971650354

In [6]:
m.prob("</s>", ["."])  # .(마침표) 뒤에 문장이 끝날 확률


Out[6]:
0.9624749529418908

In [23]:
m.probs["."]


Out[23]:
Counter({u'"': 0.028143785293581882,
         u"'": 0.0010626024652377194,
         u"''": 4.554010565304512e-05,
         u')': 0.008121318841459712,
         '</s>': 0.9624749529418908,
         u']': 0.00015180035217681706})

In [6]:
m.prob("the", ["in"])  # in 뒤에 the 가 올 확률


Out[6]:
0.26083768673815416

In [7]:
m.prob("in", ["the"])  # the 뒤에 in 이 올 확률


Out[7]:
0.0001437363613793464

In [25]:
test_sentence = ['in', 'the', '1970s', '.']
m.scoreSentence(test_sentence, verbose=True)


[['</s>'], 'in', -3.7639298908174825]
[['in'], 'the', -1.343856955005301]
[['the'], '1970s', -9.45366556371934]
[['1970s'], '.', -1.413693335308005]
[['.'], '</s>', -0.038247236076315826]
Out[25]:
-16.013392980926444

In [26]:
m.scoreSentence(["i", "am", "a", "boy", "."], verbose=True)


[['</s>'], 'i', -3.3655219750193166]
[['i'], 'am', -3.9866243623410944]
[['am'], 'a', -2.6441463991227296]
[['a'], 'boy', -7.370073198683084]
[['boy'], '.', -2.4904468301636156]
[['.'], '</s>', -0.038247236076315826]
Out[26]:
-19.895060001406158

이 모형을 기반으로 임의의 랜덤한 샘플 즉, 문장을 생성해 보면 다음과 같다.

여기에서는 하나의 단어부터 시작하여 문장의 확률이 난수값보다 크게 만드는 첫번째 단어를 찾아서 이어붙이는 방식을 취했다. 만약 최저 확률을 높이면 올바른 단어를 찾는 시간이 더 오래 걸리게 된다.


In [44]:
random.seed(1)
print(" ".join(m.generateSentence()))


basically is a little more meaningful contribution is essentially the decrease the film is nearly incoherent narrative arc ' is his plays the airport .

이번에는 한글 자료를 이용해보자 코퍼스로는 아래의 웹사이트에 공개된 Naver sentiment movie corpus 자료를 사용한다.


In [48]:
import codecs
def read_data(filename):
    with codecs.open(filename, encoding='utf-8', mode='r') as f:
        data = [line.split('\t') for line in f.read().splitlines()]
        data = data[1:]   # header 제외
    return data

train_data = read_data('/home/dockeruser/data/nsmc/ratings_train.txt')

In [49]:
from konlpy.tag import Twitter
tagger = Twitter()

def tokenize(doc):
    return ['/'.join(t) for t in tagger.pos(doc, norm=True, stem=True)]

train_docs = [row[1] for row in train_data]
sentences = [tokenize(d) for d in train_docs]

In [50]:
m = BigramModel(sentences)

In [51]:
m.prob(tokenize(u"영화")[0], tokenize(u"이"))


Out[51]:
0.37915638585360123

In [52]:
m.scoreSentence(tokenize(u"이 영화 정말 좋네"), verbose=True)


[['</s>'], 이/Noun, -4.085583056553528]
[[이/Noun], 영화/Noun, -0.9698065314249992]
[[영화/Noun], 정말/Noun, -6.034527930277947]
[[정말/Noun], 좋다/Adjective, -3.0795102003451427]
[[좋다/Adjective], '</s>', -2.165842581899055]
Out[52]:
-16.33527030050067

In [54]:
m.scoreSentence(tokenize(u"좋네 영화 이 정말"), verbose=True)


[['</s>'], 좋다/Adjective, -5.171978444505019]
[[좋다/Adjective], 영화/Noun, -2.1764847539952]
[[영화/Noun], 이/Noun, -8.361805635862364]
[[이/Noun], 정말/Noun, -8.355967542917016]
[[정말/Noun], '</s>', -4.955827057601261]
Out[54]:
-29.022063434880863

In [98]:
random.seed(24)
print("".join([w.split("/")[0] if w.split("/")[1] == "Josa" else " " + w.split("/")[0] for w in m.generateSentence()]))


 주제 넘다 오글거리다 못 하다 모든 재난은 신의 퀴즈의 연기

확률론적 언어 모형의 활용

확률론적 언어 모형은 다음과 같은 분야에 광범위하게 활용할 수 있다.

  • 철자 및 문법 교정(Spell Correction)
  • 음성 인식(Speech Recognition)
  • 자동 번역(Machine Translation)
  • 자동 요약(Summarization)
  • 챗봇(Question-Answering)